package com.googlecode.continuity.plugin.codecomment.client;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventPreview;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import com.googlecode.continuity.core.client.CoreGWT;
import com.googlecode.continuity.plugin.codecomment.client.message.CodeCommentMessages;
import com.googlecode.continuity.plugin.gwtext.client.ConfirmBox;
import com.googlecode.continuity.plugin.gwtext.client.PopupMenu;
import com.googlecode.continuity.plugin.gwtext.client.ErrorBox;
import com.googlecode.continuity.plugin.svnbrowser.client.SVNRepositoryHandle;
import com.googlecode.continuity.plugin.syntaxhighlight.client.SelectableLinesHighlightArea;
import com.googlecode.continuity.plugin.syntaxhighlight.client.SelectedLinesListener;

/**
 * CodeCommentArea
 * <p/>
 * TODO: check different startline
 * TODO: add smooth appearance for comment show, replies
 * TODO: add delete function
 *
 * @author Peter Schmitt <aragos@aragos.de>
 */
public class CodeCommentArea extends SelectableLinesHighlightArea {
// ------------------------------ FIELDS ------------------------------

  protected List comments;
  protected int commentColumn = 1;
  protected CodePointer fileInfo;
  protected HorizontalPanel contentContainer;
  private CommentBox openCommentBox;
  private HorizontalPanel fileHeaderContainer;
  private CodeCommentRPCAsync codeCommentRPC;
  private ScrollPanel linesScroller;
  private SVNRepositoryHandle svnHandle;

  private final CodeCommentMessages messages = (CodeCommentMessages) GWT.create(CodeCommentMessages.class);
  private HorizontalPanel commentTab;

// --------------------------- CONSTRUCTORS ---------------------------

  public CodeCommentArea(SVNRepositoryHandle svnHandle, CodePointer fileInfo) {
    super();
    comments = new ArrayList();
    codeColumn = 2;

    initializeContentContainer();
    initializePopupMenu();
    initializeFileHeader();
    initializeHintBar();
    getRPC();
    setFileInfo(fileInfo);
    fetchComments();
    initializeEscListener();
    this.svnHandle = svnHandle;
  }

  private void fetchComments() {
    codeCommentRPC.getCommentsForFile(fileInfo, new AsyncCallback() {
      public void onFailure(Throwable throwable) {
        (new ErrorBox(throwable)).show();
      }

      public void onSuccess(Object object) {
        addComments((List) object);
      }
    });
  }

  public void addComments(List comments) {
    for (int i = 0; i < comments.size(); i++) {
      addComment((CodeComment) comments.get(i));
    }
  }

  public void addComment(CodeComment comment) {
    comments.add(comment);

    int firstLine = comment.getLocation().getFirstLine();
    int lastLine = comment.getLocation().getLastLine();

    // add comment visually if lines are present
    if (lines.isCellPresent(line2row(firstLine), numberColumn) &&
        lines.isCellPresent(line2row(lastLine), numberColumn)) {
      for (int i = line2row(firstLine); i <= line2row(lastLine); i++) {
        lines.getFlexCellFormatter().addStyleName(i, commentColumn, "hasComment");
      }

      // filter comments starting on first line
      List comments = getCommentsForLine(firstLine);
      List onLine = new ArrayList();
      for (int i = 0; i < comments.size(); i++) {
        CodeComment aComment = ((CodeComment) comments.get(i));
        if (aComment.getLocation().getFirstLine() == firstLine) {
          onLine.add(aComment);
        }
      }

      // set number of comments and style
      lines.getFlexCellFormatter().addStyleName(line2row(firstLine), commentColumn, "hasLineComment");
      SeverityConstant severity = getHighestSeverity(onLine);
      Label label = new Label("" + onLine.size());
      label.addStyleName("commentCount");
      label.addStyleName("severity-" + severity.getName());
      lines.setWidget(line2row(firstLine), commentColumn, label);
    }
  }

  protected void removeComment(CodeComment comment) {
    comments.remove(comment);

    int firstLine = comment.getLocation().getFirstLine();
    int lastLine = comment.getLocation().getLastLine();

    // remove comment visually if lines are present
    // TODO: check if other comments are on those lines as well, this is currently wrong
    if (lines.isCellPresent(line2row(firstLine), numberColumn) &&
        lines.isCellPresent(line2row(lastLine), numberColumn)) {
      for (int i = line2row(firstLine); i <= line2row(lastLine); i++) {
        lines.getFlexCellFormatter().addStyleName(i, commentColumn, "hasComment");
      }

      // filter comments starting on first line
      List comments = getCommentsForLine(firstLine);
      List onLine = new ArrayList();
      for (int i = 0; i < comments.size(); i++) {
        CodeComment aComment = ((CodeComment) comments.get(i));
        if (aComment.getLocation().getFirstLine() == firstLine) {
          onLine.add(aComment);
        }
      }

      if (onLine.size() == 0) {
        lines.getFlexCellFormatter().removeStyleName(line2row(firstLine), commentColumn, "hasLineComment");
        lines.remove(lines.getWidget(line2row(firstLine), commentColumn));
      } else {
        // set number of comments and style
        SeverityConstant severity = getHighestSeverity(onLine);
        Label label = new Label("" + onLine.size());
        label.addStyleName("commentCount");
        label.addStyleName("severity-" + severity.getName());
        lines.setWidget(line2row(firstLine), commentColumn, label);
      }
    }
  }

  protected SeverityConstant getHighestSeverity(List comments) {
    SeverityConstant severity = (SeverityConstant) SeverityConstant.getInstances().get(0);
    for (int i = 0; i < comments.size(); i++) {
      CodeComment comment = ((CodeComment) comments.get(i));
      if (comment.getSeverity().getOrder() > severity.getOrder()) {
        severity = comment.getSeverity();
      }
    }
    return severity;
  }

  protected int line2row(int line) {
    return super.line2row(line) + 1;
  }

  private void getRPC() {
    codeCommentRPC = (CodeCommentRPCAsync) CoreGWT.getRPC("CodeCommentRPCAsync");
  }

  private void initializeContentContainer() {
    contentContainer = new HorizontalPanel();
    area.add(contentContainer);
    area.remove(lines);
    linesScroller = new ScrollPanel(lines);
    contentContainer.add(linesScroller);
    // force scrollbars for IE7
    DOM.setStyleAttribute(linesScroller.getElement(), "overflow", "scroll");
  }

  private void initializeEscListener() {
    DOM.addEventPreview(new EventPreview() {
      public boolean onEventPreview(Event event) {
        if (DOM.eventGetType(event) == Event.ONKEYDOWN && DOM.eventGetKeyCode(event) == 27) {
          hideCommentBox();
          return false;
        }
        return true;
      }
    });
  }

  public void hideCommentBox() {
    if (openCommentBox == null) {
      return;
    }
    if (openCommentBox.isEdited()) {
      ConfirmBox confirmBox = new ConfirmBox(messages.confirmDiscardComment(),
          new ClickListener() {
            public void onClick(Widget sender) {
              hideCommentBoxFinal();
            }
          }, messages.yes(), messages.no());
      confirmBox.show();
      return;
    }
    hideCommentBoxFinal();
  }

  private void initializeFileHeader() {
    fileHeaderContainer = new HorizontalPanel();
    commentTab = new HorizontalPanel();

    Label name = new Label();
    Label lines = new Label();
    Label revision = new Label();
    Label commentTabText = new Label();
    Image closeComment = new Image(CoreGWT.getWebappRoot() + "img/close_12.png");

    commentTab.add(commentTabText);
    commentTab.add(closeComment);

    fileHeaderContainer.add(name);
    fileHeaderContainer.add(lines);
    fileHeaderContainer.add(revision);
    fileHeaderContainer.add(commentTab);

    closeComment.addClickListener(new ClickListener() {
      public void onClick(Widget widget) {
        hideCommentBox();
      }
    });

    fileHeaderContainer.addStyleName("fileHeader");
    commentTab.addStyleName("commentTab");
    name.addStyleName("name");
    name.setWordWrap(false);
    lines.addStyleName("lines");
    lines.setWordWrap(false);
    revision.addStyleName("revision");
    revision.setWordWrap(false);
    commentTabText.addStyleName("tabText");
    commentTabText.setWordWrap(false);
    closeComment.addStyleName("closeComment");

    commentTab.setVisible(false);
    fileHeaderContainer.setCellWidth(commentTab, "100%");
    fileHeaderContainer.setCellHorizontalAlignment(commentTab, HasHorizontalAlignment.ALIGN_RIGHT);
    fileHeaderContainer.setCellVerticalAlignment(commentTab, HasVerticalAlignment.ALIGN_BOTTOM);

    area.insert(fileHeaderContainer, 0);
  }

  private void initializeHintBar() {
    Image arrow = new Image(CoreGWT.getWebappRoot() + "img/hint_arrow.png");
    Label hint = new Label(messages.commentHint());

    lines.setWidget(0, 1, hint);
    lines.setWidget(0, 0, arrow);

    arrow.addStyleName("arrow");
    hint.addStyleName("hint");
    lines.getFlexCellFormatter().setVerticalAlignment(0, 2, HasVerticalAlignment.ALIGN_MIDDLE);
    lines.getFlexCellFormatter().setHorizontalAlignment(0, 0, HasHorizontalAlignment.ALIGN_RIGHT);
    lines.getFlexCellFormatter().setColSpan(0, 0, 2);
    lines.getRowFormatter().addStyleName(0, "hintBar");
  }

  private void initializePopupMenu() {
    this.addSelectedLinesListener(new SelectedLinesListener() {
      public void onLinesSelected(Event cause, final int first, final int last) {
        // highlight selected lines
        addLinesStyle(first, last + 1, "viewedLine");

        PopupMenu menu = new PopupMenu();

        // ensure lines are unselected when menu closes
        menu.addPopupListener(new PopupListener() {
          public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
            if (autoClosed) {
              removeLinesStyle(first, last + 1, "viewedLine");
            }
          }
        });

        // a click occured (in difference to a selection of multiple lines)
        if (first == last) {
          List presentComments = getCommentsForLine(first);

          // add a line to the menu for each comment on selected line
          for (int i = 0; i < presentComments.size(); i++) {
            final CodeComment comment = (CodeComment) presentComments.get(i);
            final int lastLine = comment.getLocation().getLastLine();
            final int firstLine = comment.getLocation().getFirstLine();
            int replyCount = comment.getReplies().size();
            String explanation =
                firstLine != lastLine ? messages.linesXthroughY(firstLine, lastLine) : messages.lineX(firstLine);
            if (replyCount == 1) {
              explanation += messages.oneReply();
            } else if (replyCount > 1) {
              explanation += messages.multipleReplies(replyCount);
            }
            menu.addLine(
                comment.getSeverity().getName(),
                new ClickListener() {
                  public void onClick(Widget widget) {
                    showComment(comment);
                  }
                },
                "severity-" + comment.getSeverity().getName(),
                explanation
            );
          }
        }
        menu.addSeperator();

        // add a line that offers to create a new comment on the selected line(s)
        menu.addLine(
            messages.addComment(),
            new ClickListener() {
              public void onClick(Widget widget) {
                createComment(first, last);
              }
            },
            null,
            first != last ? messages.linesXthroughY(first, last) : messages.lineX(first)
        );
        menu.showAt(DOM.eventGetClientX(cause) + Window.getScrollLeft(),
            DOM.eventGetClientY(cause) + Window.getScrollTop());
      }
    });
  }

  public void createComment(final int first, final int last) {
    final CodeComment comment = new CodeComment();
    CodePointer pointer = new CodePointer();
    pointer.setFirstLine(first);
    pointer.setLastLine(last);
    pointer.setVersion(fileInfo.getVersion());
    pointer.setUrl(fileInfo.getUrl());
    pointer.setName(fileInfo.getName());
    comment.setLocation(pointer);

    showCommentBox(new EditCommentBox(svnHandle, comment, this));
  }

  protected List getCommentsForLine(int lineNumber) {
    // TODO: make more efficient
    List lineComments = new ArrayList();
    for (int i = 0; i < comments.size(); i++) {
      CodeComment comment = ((CodeComment) comments.get(i));
      if (comment.getLocation().getFirstLine() <= lineNumber &&
          comment.getLocation().getLastLine() >= lineNumber) {
        lineComments.add(comment);
      }
    }
    return lineComments;
  }

  public void showComment(CodeComment comment) {
    // since these comments get selected on one line but can span more, we have to extend the higlighted lines
    addLinesStyle(comment.getLocation().getFirstLine(), comment.getLocation().getLastLine() + 1, "viewedLine");

    showCommentBox(new ShowCommentBox(svnHandle, comment, this));
  }

  public void showCommentBox(final CommentBox box) {
    // check whether another comment is already open, if it is edited: request confirmation to close,
    // else close right away
    if (openCommentBox != null) {
      if (openCommentBox.isEdited()) {
        ConfirmBox confirmBox = new ConfirmBox(messages.confirmDiscardComment(),
            new ClickListener() {
              public void onClick(Widget sender) {
                hideCommentBoxFinal();
                showCommentBoxFinal(box);
              }
            }, messages.yes(), messages.no());
        confirmBox.show();
        return;
      }
      hideCommentBox();
    }
    showCommentBoxFinal(box);
  }

  public void hideCommentBoxFinal() {
    if (openCommentBox == null) {
      return;
    }

    // remove line highlighting for commented lines
    removeLinesStyle(openCommentBox.getComment().getLocation().getFirstLine(),
        openCommentBox.getComment().getLocation().getLastLine() + 1, "viewedLine");

    contentContainer.remove(openCommentBox);

    // hide comment tab
    Label tab = (Label) commentTab.getWidget(0);
    tab.setText("");
    commentTab.setVisible(false);
    commentTab.removeStyleName("severity-" + openCommentBox.getComment().getSeverity().getName());

    // remove as open comment
    openCommentBox = null;
    linesScroller.setWidth(area.getOffsetWidth() + "px");
  }

  private void showCommentBoxFinal(CommentBox box) {
    openCommentBox = box;
    contentContainer.add(box);

    // IE7 hack, IE7 doesn't like height:100%
    box.setHeight(contentContainer.getOffsetHeight() + "px");

    // adjust linesscroller
    linesScroller.setWidth((linesScroller.getOffsetWidth() - box.getOffsetWidth()) + "px");

    // show comment tab
    int first = box.getComment().getLocation().getFirstLine();
    int last = box.getComment().getLocation().getLastLine();
    Label tab = (Label) commentTab.getWidget(0);
    tab.setText(first != last ? messages.commentForLinesXthroughY(first, last) : messages.commentForLineX(first));
    commentTab.setVisible(true);
    if (box instanceof ShowCommentBox) {
      commentTab.addStyleName("severity-" + box.getComment().getSeverity().getName());
    }
  }

  public void setHeight(String string) {
    super.setHeight(string);
    int height = Integer.parseInt(string.substring(0, string.length() - 2));
    linesScroller.setHeight((height - fileHeaderContainer.getOffsetHeight()) + "px");
  }

  public void setFileInfo(CodePointer fileInfo) {
    this.fileInfo = fileInfo;
    setStartLine(fileInfo.getFirstLine());

    ((Label) fileHeaderContainer.getWidget(0)).setText(messages.fileName(fileInfo.getName()));
    ((Label) fileHeaderContainer.getWidget(1)).setText(fileInfo.getFirstLine() != fileInfo.getLastLine() ?
        messages.fileLines(fileInfo.getFirstLine(), fileInfo.getLastLine()) :
        messages.fileLine(fileInfo.getFirstLine()));
    ((Label) fileHeaderContainer.getWidget(2)).setText(messages.fileRevision(fileInfo.getVersion()));
  }

// --------------------- GETTER / SETTER METHODS ---------------------

  public CodePointer getFileInfo() {
    return fileInfo;
  }

// -------------------------- OTHER METHODS --------------------------

  public int addLine(String lineCode) {
    int rowCount = super.addLine(lineCode);
    int lineNumber = row2line(rowCount);

    lines.getFlexCellFormatter().addStyleName(rowCount, commentColumn, "comment");
    lines.getFlexCellFormatter().addStyleName(rowCount, commentColumn, "gutter");
    createGutterListener(rowCount, commentColumn);

    // add comments
    List comments = getCommentsForLine(lineNumber);
    if (comments.size() > 0) {
      lines.getFlexCellFormatter().addStyleName(rowCount, commentColumn, "hasComment");

      // filter comments starting on current line
      List onLine = new ArrayList();
      for (int i = 0; i < comments.size(); i++) {
        CodeComment aComment = ((CodeComment) comments.get(i));
        if (aComment.getLocation().getFirstLine() == lineNumber) {
          onLine.add(aComment);
        }
      }

      // set number of comments and style
      if (onLine.size() > 0) {
        lines.getFlexCellFormatter().addStyleName(rowCount, commentColumn, "hasLineComment");
        SeverityConstant severity = getHighestSeverity(onLine);
        Label comment = new Label("" + onLine.size());
        comment.addStyleName("commentCount");
        comment.addStyleName("severity-" + severity.getName());
        lines.setWidget(rowCount, commentColumn, comment);
      }
    }
    return rowCount;
  }

  protected int row2line(int row) {
    return super.row2line(row) - 1;
  }

  public int getLineCount() {
    return lines.getRowCount() - 1;
  }

  // override necessary since we can't probe first line anymore - it's occupied by the hintbar
  protected int getLineHeight() {
    return lines.getWidget(1, codeColumn).getOffsetHeight();
  }

  public void setWidth(String string) {
    super.setWidth(string);
    linesScroller.setWidth(string);
  }

  public boolean showingEditedComment() {
    return openCommentBox != null && openCommentBox.isEdited();
  }
}
